Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add board: Seeed XIAO BLE #2591

Merged
merged 7 commits into from
Jan 28, 2025
Merged

Add board: Seeed XIAO BLE #2591

merged 7 commits into from
Jan 28, 2025

Conversation

ssievert42
Copy link
Contributor

@ssievert42 ssievert42 commented Jan 5, 2025

This adds support for the Seeed XIAO BLE, a tiny development board with a nRF52840 and 2 MB of external flash.

The board definition file is based on this one by Espruino forums user parasquid.

Some goodies that are included as well:

  • the build generates an UF2 image, that can be flashed to the board when it is in bootloader mode, without needing to install anything on the host (connect via USB, double press the reset button, then copy the file to the USB drive that appears)
  • automagically built UF2 image with GitHub actions
  • E.rebootToDFU() triggers a reboot to UF2 bootloader mode

@ssievert42 ssievert42 changed the title add board: Seeed XIAO BLE Add board: Seeed XIAO BLE Jan 6, 2025
@gfwilliams
Copy link
Member

Thanks - this looks great!

Do you think it'd be possible to change jswrap_espruino_rebootToUF2 just to use jswrap_espruino_rebootToDFU though? There's already the stuff in there for it so all you'd have to do is rename jshRebootToUF2Bootloader to jshRebootToDFU in jshardware.c and change the "ifdef" : "STM32F4", line to "#if" : "defined(STM32F4) || defined(ESPR_HAS_BOOTLOADER_UF2)",

@ssievert42 ssievert42 force-pushed the xiaoble branch 4 times, most recently from 6fc4180 to 44d2473 Compare January 6, 2025 17:05
@ssievert42
Copy link
Contributor Author

Thanks for the feedback!
I've now dropped E.rebootToUF2() in favor of E.rebootToDFU(), mentioned the UF2 bootloader mode in the docs of E.rebootToDFU() and changed the other stuff as you suggested - no need to have two functions with slightly different names that basically do the same thing :)

@ssievert42
Copy link
Contributor Author

Does the board definition file need to use pinutils.generate_pins()?
I don't remember why I decided to change get_pins() to not use pinutils.generate_pins(), but I think the reason was to have pin names that are the same as the ones printed on the board.

"link": ["https://www.seeedstudio.com/Seeed-XIAO-BLE-nRF52840-p-5201.html"],
"default_console": "EV_USBSERIAL",
"variables": 14000, # How many variables are allocated for Espruino to use. RAM will be overflowed if this number is too high and code won't compile.
"binary_name": "espruino_%v_xiaoble.hex",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this is .uf2? It may be you don't need the ifdef XIAOBLE ... proj: $(PROJ_NAME).uf2 line if it is...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoding only the XIAOBLE to create a UF2 in that makefile does seem rather hacky...
Turns out I needed to change a bit more; something kept adding ".hex" to the end of the binary name.
See d40b0ca for what I did to make this work :)

@@ -2887,6 +2887,14 @@ void jshReboot() {
NVIC_SystemReset();
}

#ifdef ESPR_HAS_BOOTLOADER_UF2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, thanks! Please could you add a comment in https://github.com/espruino/Espruino/blob/master/README_BuildProcess.md#infomakefile-definitions though? I'm trying to keep all the defines documented somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added 👍
Reminder for future me: Do this for BLE private address support as well.

src/jsflash.c Outdated
@@ -1374,6 +1374,7 @@ JsVar *jsfGetBootCodeFromFlash(bool isReset) {

bool jsfLoadBootCodeFromFlash(bool isReset) {
// Load code in .bootFirst at first boot UNLESS BTN1 IS HELD DOWN (BTN3 for Dickens)
#ifndef ESPR_NO_BOOT_JS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shame the board doesn't have a button so this is needed - but I feel like it's very unlikely anyone would make a custom build that includes this?

Maybe you could:

  • Make the UF2 include the flash area used for saved code so it always overwrites it
  • Make a special UF2 that just overwrites the saved code area to allow you to clear it if you want to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I feel like it's very unlikely anyone would make a custom build that includes this

Yeah, you're probably right.
The board does have a (reset) button; pressing it just immediately resets the board...

Make the UF2 include the flash area used for saved code

Probably not an option if saved code is on SPI flash though 🙈
(And I really want to use those 2 MB of external flash.)

Make a special UF2 that just overwrites the saved code area

I'd rather have something that allows recovering other data saved to storage, but I guess this will be the way to go if you don't like the "pull a magic pin to ground on boot" thing I added with my latest push.

@gfwilliams
Copy link
Member

Thanks for those changes to UF2 reboot - that looks really good.

Does the board definition file need to use pinutils.generate_pins()? ... I think the reason was to have pin names that are the same as the ones printed on the board.

No, that's fine - generate_pins was just a utility function to generate a bunch of contiguous pins. If you want to renumber the pins so they match the board what you've done is the right way to handle it.

@ssievert42
Copy link
Contributor Author

ssievert42 commented Jan 14, 2025

Thanks for the review!
I think I've addressed most of your comments;
The changes I just pushed do the following:

  • if the "binary_name" in a board definition file ends with ".uf2", we build an UF2 image and pass chip.part (in the case of the XIAOBLE NRF52840) to the uf2conf.py script as the "familyID" argument
  • drop ESPR_NO_BOOT_JS from my previous stuff in this PR
  • add ESPR_SKIP_BOOT_JS_PIN to set a pin that can be pulled to ground on boot to skip running boot js (seems to work reasonably well, even without adding a delay between setting the pin mode and reading the pin value)

If you think that the whole skipping boot js stuff is not that good of an idea I'll just drop that for now and look into the other recovery option you suggested, where we'd have an "I messed up and want to reset my board" build.

@gfwilliams
Copy link
Member

Thanks! This is looking really good.

ESPR_SKIP_BOOT_JS_PIN

As D1 is configured as the button in the bootloader anyway, could we not just add BTN1' : { 'pin' : 'D1' , 'inverted' : 1} to the BOARD.py file, and this would have exactly the same effect, without having to have ESPR_SKIP_BOOT_JS_PIN? There are some other things we do (like clearing bonded peers) if booted with BTN1 held down so it might be preferable to do that route?

@ssievert42
Copy link
Contributor Author

ssievert42 commented Jan 24, 2025

Minimal code changes to achieve the same thing, sounds neat!
However, for BTN1 to have any effect on whether boot js is run or not, I had to add a #if defined(XIAOBLE) to jsflash.c. (See the latest commit that I pushed.)

I don't really like having those #ifdefs refer to the board though, which is why I originally ended up with ESPR_SKIP_BOOT_JS_PIN.
But I don't want to break other boards by changing bootup behaviour either...
Maybe we could have something along the lines of a "make BTN1 skip boot js"-define in the board file?.

Do you think having D1 / BTN1 as IN_PULLDOWN or IN_PULLUP would be better?
I figured that it's probably IN_PULLDOWN, as having (weak) ground on a pin can probably cause less weird behavior from something connected to that pin than pulling that pin high.

@jj12
Copy link

jj12 commented Jan 25, 2025

Thank you both for your work 😄

When trying the firmware from the build artifacts, I noticed that some pins are not working. D6 to D10 have a wrong num assigned: instead of 11-15 must have values 43-47, as per variant.cpp.

Also: The the pin definitions >= D12 have different pin numbers assigned than mentioned in variant.cpp or the pinout sheet. This might be mostly cosmetic, but poses an inconsistency with Seeed's docs.

(Unfortunately, I'm not yet able to get the build process running myself, via dockerfile. Compiling any board fails with Syntax error: Unterminated quoted string, will have to keep looking.)

@ssievert42
Copy link
Contributor Author

@jj12 Thanks, nice catch!
The pin definitions should now match Seeed's docs; I was going purely by the shematic and apparently can't really read those (and assumed that P0.N or P1.N both meant pin N) and just assigned pin numbers to the leftover pins at will 🙈
(I've checked pins D0 through D10 with a multimeter and digitalWrite(D0, 1) just to be sure though, and apart from D1, where you've got to do D1.mode("output") first since it's defined as BTN1, everything seems fine :) )

About your build issues: Just that message isn't a lot to go by, but can you build from master, or does this PR introduce those errors?
I've had to deal with some weird build behaviour myself, maybe you can find something that helps in that issue.
Just a stab in the dark: maybe it's an issue with Git changing line endings?

@jj12
Copy link

jj12 commented Jan 26, 2025

Yup, can confirm, all pins work now. Thanks!

Building is not related to your PR, it was based on master and the provided Dockerfile on RPi4. Also now tried your steps from the other issue, but didn't work (new error this time...). Might try on x86, and if that does not work, I will open a separate issue.

@gfwilliams
Copy link
Member

Do you think having D1 / BTN1 as IN_PULLDOWN or IN_PULLUP would be better?

I think pulldown is probably best as well - otherwise we'd have to get it to 'invert' the state in software as well.

adding ifdef to jsflash

Have you tried without those IFDEFs? It might have been a bit of a red herring - because normally we detect the button press at boot through:

https://github.com/espruino/Espruino/blob/master/targets/nrf5x/main.c#L34
https://github.com/espruino/Espruino/blob/master/src/jsinteractive.c#L889-L897

So I think if you wrote code to the device and then booted it while holding the button, it wouldn't be executed even without those ifdefs in jsflash.

We also have this idea of .bootrst and .bootcde (https://www.espruino.com/Saving) - sometimes folks want to force code to always run even when the button is pressed (eg if in a product you don't want a customer to be able to boot up with nothing running), but we make it nontrivial to write to .bootrst in the IDE so it's ok if it can make it hard to recover from (it's the same in any board).

I think the ifdefs for BANGLEJS are in there because in that case we wanted a way to boot into the recovery menu and ensure that absolutely nothing ever ran - but that's kind of an exception because we have an actual UI that can be displayed for recovery

Docker

Yes, the Docker build is a bit annoying and honestly I'm tempted to just remove that file. It was contributed but then not maintained, and given Github builds are just so easy, I think it's probably just wasting people's time having something that's not working in the repo.

@ssievert42
Copy link
Contributor Author

ssievert42 commented Jan 27, 2025

I think pulldown is probably best as well

Pulldown it is 👍

Have you tried without those IFDEFs?

Just tried again, without those #ifdefs boot js is executed no matter what I do.
For testing, I've written

let a = 42;

to .boot0 and then check if a is set.

To simulate a button between pin D1 and 3V3, I've got a cable that bridges those pins on a breadboard.
With that cable BTN1.read() returns true and without it BTN1.read() returns false, so it's definitely acting as a button.

Things that do nothing while having D1 connected to 3V3:

  • not supplying any power to the board and then plugging in a USB cable
  • with a USB cable plugged in, pressing the onboard reset button
  • calling E.rebootToDFU() and flashing the same firmware again

With those #ifdefs however, these all lead to .boot0 not being executed.

I think the issue is that we read the button state in main.c and then call jsiInit(bool autoLoad)

jsiInit(!buttonState /* load from flash by default */); // pressing USER button skips autoload

and from that call jsiSemiInit(bool autoLoad, JsfFileName *loadedFilename)
jsiSemiInit(autoLoad, NULL/* no filename */);

where we do respect autoload
bool loadFlash = autoLoad && jsfFlashContainsCode();
if (loadFlash) {
jsiStatus &= ~JSIS_COMPLETELY_RESET; // loading code, remove this flag
jspSoftKill();
jsvSoftKill();
jsfLoadStateFromFlash();
jsvSoftInit();
jspSoftInit();
}

but a few lines further down then invoke jsiSoftInit(bool hasBeenReset)
jsiSoftInit(!autoLoad);

which in turn calls jsfLoadBootCodeFromFlash(bool isReset)
jsfLoadBootCodeFromFlash(hasBeenReset);

where we finally ignore isReset and load and execute .boot0 unconditionally, setting a to 42:

Espruino/src/jsflash.c

Lines 1393 to 1410 in 672e959

#if defined(BANGLEJS)
#if defined(DICKENS)
if (!(jshPinGetValue(BTN3_PININDEX)==BTN3_ONSTATE &&
(jsiStatus & JSIS_FIRST_BOOT)))
#else // not DICKENS
if (!(jshPinGetValue(BTN1_PININDEX)==BTN1_ONSTATE &&
(jsiStatus & JSIS_FIRST_BOOT)))
#endif
#endif
{
char filename[7] = ".bootX";
for (int i=0;i<4;i++) {
filename[5] = (char)('0'+i);
JsVar *code = jsfReadFile(jsfNameFromString(filename),0,0);
if (code)
jsvUnLock2(jspEvaluateVar(code,0,0), code);
}
}

So to sum it up, we do:

bool buttonState = jshPinGetValue(BTN1_PININDEX) == BTN1_ONSTATE;
bool isReset = !!buttonState;
{
  char filename[7] = ".bootX";
  for (int i=0;i<4;i++) {
    filename[5] = (char)('0'+i);
    JsVar *code = jsfReadFile(jsfNameFromString(filename),0,0);
    if (code)
      jsvUnLock2(jspEvaluateVar(code,0,0), code);
  }
}

And adding a

if (isReset) {
  return false;
}

to the beginning of jsfLoadBootCodeFromFlash() seems to also result in .boot0 not being executed.

So the solution would be to replace the existing #ifdefs with

if (!(isReset &&
     (jsiStatus & JSIS_FIRST_BOOT)))

and thus keep us from entering the block that loads boot code on all boards if BTN1 (or BTN3 for DICKENS, or really whatever is defined in main.c) is held down, right?

Still kind of paranoid about breaking other boards though 😅
And such a change is probably something best left for another PR anyways.

Unless of course I've missed something 🙈

@jj12 If you haven't already figured this out yourself: Looks like scripts/provision.sh fetches an x86_64 arm-none-eabi-gcc, which doesn't work on a Pi.
You cloud try with the following Dockerfile (worked for me on a Pi 5):

FROM debian:bookworm

RUN apt-get update && apt-get upgrade -y && apt-get install -y build-essential gcc-arm-none-eabi tar python3 python3-pip python-is-python3 git curl unzip && apt-get clean
RUN pip install --break-system-packages protobuf==3.17.3
RUN pip install --break-system-packages --ignore-requires-python --no-deps nrfutil==6.1.7
RUN pip check | sed -e 's/^.*requires \(.*\),.*/\1/' > requirements.txt && pip install --break-system-packages -r requirements.txt && rm requirements.txt
RUN git config --global --add safe.directory '*'
RUN mkdir /espruino
RUN git clone 'https://github.com/espruino/Espruino.git' /espruino
WORKDIR /espruino
RUN bash -c 'source scripts/provision.sh BANGLEJS2'
RUN bash -c 'source scripts/provision.sh PUCKJS'
RUN bash -c 'source scripts/provision.sh EMSCRIPTEN2'
RUN bash -c 'source scripts/provision.sh JOLTJS'

and remove/ replace the calls to scripts/provision.sh with the boards you want to build for, and build it with

docker build -t espruino_builder

You can then run the container like this:

mkdir -p build_output
docker run --rm -it -v "$(pwd)"/build_output:/espruino/bin espruino_builder /bin/bash

and finally build for Bangle.js 2:

make clean
BOARD=BANGLEJS2 RELEASE=1 DFU_UPDATE_BUILD=1 make

or Linux:

make clean
make

Hope that helps :)

@gfwilliams
Copy link
Member

.boot0

I think this is exactly as expected. It's a bit like what I said with .bootrst but the process is all documented at: https://www.espruino.com/Saving#boot-process

When you save to flash, the IDE saves to .bootcde which really is only executed when the button isn't held down, so you're safe.

If you write to .boot0 deliberately then yes, that is always executed - but there are reasons some people want that, and you'd have to deliberately choose to write to it instead of the IDE's defaults.

So I'll take out those extra IFDEFs so this port behaves like all the others and merge this in - you could maintain your own version with this change but I'd argue it's probably a lot easier just to write to .bootcde :)

Docker

Thanks for spotting about the compiler download. Actually if arm-gcc exists on the path then provision won't try and download it, so probably the easiest is just to ensure that apt install gcc-arm-none-eabi comes before provision. Having said that different GCC versions have staggeringly different binary sizes and performance, which is why we specifically choose one - so even if you can build using docker, you may find that building for some devices (where storage space is very tight) still fails.

@gfwilliams gfwilliams merged commit a3f61a0 into espruino:master Jan 28, 2025
22 checks passed
@ssievert42
Copy link
Contributor Author

Thanks!

.bootcde

Alright, so that's what I missed 🤦
Probably should have read the boot process docs you already linked a few comments back 🙈

Docker

The provision script does print a nice huge warning message, that complains about the arm-gcc on the path having the wrong version; interesting that the difference between GCC versions can be so significant though.

@jj12
Copy link

jj12 commented Jan 31, 2025

Docker

@ssievert42 Thanks a lot for the detailed information on your Dockerfile and steps, I was able to create my own build.
The firmware is indeed bigger than the github artifact, for the NRF52840 no issue, but for the NRF52832 (MDBT42Q) it results in code and storage overlap (as already hinted by Gordon for devices with less storage).

Some goodies

Finally also tried E.rebootToDFU(), this is so much nicer than trying to hit the mini reset button!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants